/*
 * Spider.cpp
 *
 *  Created on: 2010-8-28
 *      Author: anod
 */
#include <string>

#include <QObject>
#include <QHttp>
#include <QUrl>
#include <QFile>
#include <QCryptographicHash>

#include <Module-AppData/Spider.hpp>
#include <IOEvent.hpp>

using std::string;

#ifndef CACHE_DIR	//下载的文件临时文件夹目录
#define CACHE_DIR "/tmp/weather/"
#endif

/**
 * 类SpiderImpl封装了QHttp的控制功能
 */
class SpiderImpl:public QObject
{
	Q_OBJECT
public:
	// $$构造函数，需要一个Spider在slot被调用的时候发布IOEvent
	SpiderImpl( AppData::Spider *_p )
	:get_actID(0),
	 http_thread( new QHttp(this) ),
	 dev( new QFile(this) ),
	 parent(_p)
	{
		// do some connect for signal and slot
		connect(http_thread, SIGNAL(requestFinished(int, bool)), this, SLOT(onDone(int, bool)));
	}

	// $$析构函数，需要在移除的时候把请求的资源清除
	virtual ~SpiderImpl()
	{
		if(http_thread != NULL)
		{
			if( http_thread->hasPendingRequests() )	clear();
			delete http_thread;
			http_thread = NULL;
		}
		if( dev != NULL )
		{
			if( dev->isOpen() ) dev->close();
			delete dev;
			dev = NULL;
		}
	}
public:
	// 调用QHttp的get方法，发送HTTP请求到服务器
	// 传进来的_url已经进行了检查测试了，能够保证如下形式：
	//	http(s)://domain.name/index.html?key=value
	//  1保证有scheme且只有可能为http/https
	//  2有host
	//  3含有filename的path，如果是默认也有index.html
	//  4如果有query string也会带上
	void doGet( const QString &_url, unsigned char *_buffer, size_t _buflen )
	{
		p_buff = _buffer;
		buff_len = _buflen;

		QUrl url(_url);

		if( url.scheme().toLower()=="https" )
			http_thread->setHost(url.host(), QHttp::ConnectionModeHttps, url.port(80));
		else http_thread->setHost(url.host(), url.port(80));
		int pos = _url.indexOf( url.host() ) + url.host().length();

		// 填充http请求，请求格式参考的是flashget
		get_actID = http_thread->get( _url.mid(pos) );
		url_loading = _url;
	}

	// 基本上和doGet差不多，但是多了一点就是它会把内容保存在文件fname里面
	const char* doGetSave( const QString &_url, unsigned char *_buffer, size_t _buflen, const string &fname )
	{
		unsigned char stack_mem[sizeof(IOEvent)];
		Event *pevent;

		p_buff = _buffer;
		buff_len = _buflen;

		dev->setFileName( fname.c_str() );
		if( !dev->open(QIODevice::ReadWrite|QIODevice::Truncate) )
		{
			pevent = new(stack_mem) IOEvent(
				Event::IOEVENT_LOAD_FAILED,
				fname.c_str(),
				dev->error(),
				"cannot open the file",
				NULL
			);
			parent->eventHappen(*pevent);
			pevent->~Event();
			return NULL;//不成功还是返回空
		}

		QUrl url(_url);
		if( url.scheme().toLower()=="https" )
			http_thread->setHost(url.host(), QHttp::ConnectionModeHttps, url.port(80));
		else http_thread->setHost(url.host(), url.port(80));
		int pos = _url.indexOf( url.host() ) + url.host().length();

		QHttpRequestHeader req;
		req.setRequest( "GET", _url.mid(pos) );
		req.addValue(  "Host",       url.host()  );
		req.addValue(  "Accept",     "*/*"  );
		req.addValue(  "Referer",    _url.left(pos)  );
		req.addValue(  "User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"  );
		req.addValue(  "Connection", "Keep-Alive"  );
		get_actID = http_thread->get( _url.mid(pos), dev );
		url_loading = _url;

		return dev->fileName().toStdString().c_str();
	}

	// 从文件fname中读取数据到缓冲区
	const char* loadFile( const string &fname, unsigned char *_buffer, size_t _bufflen )
	{
		unsigned char stack_mem[sizeof(IOEvent)];
		Event *pevent;

		dev->setFileName( fname.c_str() );
		if( !dev->open(QIODevice::ReadOnly) )
		{
			pevent = new(stack_mem) IOEvent(
				Event::IOEVENT_LOAD_FAILED,
				fname.c_str(),
				dev->error(),
				"cannot open the file",
				NULL
			);
			parent->eventHappen(*pevent);
			pevent->~Event();
			return NULL;//不成功还是返回空
		}


		int bytes_load = dev->bytesAvailable();
		if( _buffer!=NULL )
			dev->read(reinterpret_cast<char*>(_buffer), _bufflen);
		dev->close();
		pevent = new(stack_mem) IOEvent(
			Event::IOEVENT_LOAD_FINISH,
			fname.c_str(),
			bytes_load,
			NULL,
			reinterpret_cast<const char*>(_buffer)
		);
		parent->eventHappen( *pevent );
		pevent->~Event();
		return dev->fileName().toStdString().c_str();
	}

	// 得到QHttp当前的状态方便进行调整（有可能会发生上次请求到下次再被调用的时候网速过慢而还在连接当中
	QHttp::State state() const
	{
		return http_thread->state();
	}

	// 停止QHttp当前的任务，当然没有做条件判定，我认为Qt在实现的时候应该已经做的判定了，我就不用做了
	void clear()
	{
		get_actID = 0;
		http_thread->clearPendingRequests();
		http_thread->abort();
		http_thread->close();
	}

	// 槽，主要针对QHttp在发送GET请求之后有可能会得到的一些这个程序用的到的信号来进行处理
public slots:
	void onDone( int id, bool error )
	{
		unsigned char stack_mem[sizeof(IOEvent)];
		if( id == get_actID && !error )
		{
			QIODevice *pdev = http_thread->currentDestinationDevice();
			int bytes_load;
			if( pdev==NULL )
			{
				bytes_load = http_thread->bytesAvailable();
				if( p_buff!=NULL )
					http_thread->read(reinterpret_cast<char*>(p_buff), buff_len);
			}
			else
			{
				pdev->reset();
				bytes_load = pdev->bytesAvailable();
				if( p_buff!=NULL )
					pdev->read(reinterpret_cast<char*>(p_buff), buff_len);
				pdev->close();
			}

			Event *pevent = new(stack_mem) IOEvent(
				Event::IOEVENT_LOAD_FINISH,
				url_loading.toStdString().c_str(),
				bytes_load,
				NULL,
				reinterpret_cast<char*>(p_buff)
			);
			parent->eventHappen( *pevent );
			pevent->~Event();
			url_loading.clear();
		}
		else if( error )
		{
			Event *pevent = new(stack_mem) IOEvent(
				Event::IOEVENT_LOAD_FAILED,
				url_loading.toStdString().c_str(),
				http_thread->error(),
				http_thread->errorString().toStdString().c_str(),
				NULL
			);
			parent->eventHappen( *pevent );
			pevent->~Event();
			url_loading.clear();

			if( http_thread->hasPendingRequests() )
				clear();
		}
	}
private:
	friend class Spider;
	int    get_actID;		//在调用QHttp::get的操作号
	QHttp *http_thread;
	QFile *dev;				//下载的文件
	QString url_loading;

	AppData::Spider *parent;
	unsigned char   *p_buff;
	size_t          buff_len;
};

namespace AppData
{

Spider::Spider()
:impl_( new SpiderImpl(this) )
{}

Spider::~Spider()
{
	if( impl_!=NULL )
	{
		delete impl_;
		impl_ = NULL;
	}
}

/**
 * 实现方式：
 * 1 关掉正在执行的请求
 * 2 修正url，让它符合格式
 * 3 进行新请求
 */
void Spider::grab(const QString &url, unsigned char *buffer, size_t buflen)
{
	if( buffer==NULL ) return;
	if( impl_->state()!=QHttp::Unconnected )//可用
	{
		impl_->clear();
	}

	unsigned char stack_mem[sizeof(IOEvent)];
	Event *pevent;

	// 检查url的合法性，包括：
	// 1 是否具有http/https的scheme
	// 2 是否有host
	// 3 是否有filename接在path上
	QUrl URL( url );
	if( !URL.isValid() )//第一关都过不了
	{
		// 发送load failed事件
		pevent = new(stack_mem) IOEvent(
			Event::IOEVENT_LOAD_FAILED,
			url.toStdString().c_str(),
			1,
			"invalid url to grab",
			NULL
		);
		eventHappen( *pevent );
		pevent->~Event();
		return;
	}
	if( URL.scheme().isEmpty() )//没有协议类型
	{
		URL = "http://"+url;
	}
	if( URL.host().isEmpty() )//没有host，我自认为你在本地架了个服务器
	{
		URL.setHost( "localhost" );
	}
	if( URL.path().isEmpty() || URL.path().right(1)=="/" )//没有文件名，按照http我给你加index
	{
		URL.setPath( URL.path()+"index.html" );
	}

	// 最后如果不是http(s)协议，我就没有办法帮你忙了
	if( URL.scheme().toLower().left(4) == "http" )
		impl_->doGet( URL.toString(), buffer, buflen );
	else
	{
		// 发送load failed事件
		pevent = new(stack_mem) IOEvent(
			Event::IOEVENT_LOAD_FAILED,
			url.toStdString().c_str(),
			2,
			"url's scheme must be http(s)",
			NULL
		);
		eventHappen( *pevent );
		pevent->~Event();
	}
}

/**
 * 实现方式：
 * 1 得到url的哈希值
 * 2 检查这个哈希值是否已经存在于url_got
 * 3 如果已经存在，就从文件中读取
 * 4 如果不存在，调用私有属性的方法获得数据
 */
const char* Spider::download(const QString &url, unsigned char *buffer, size_t buflen)
{
	unsigned char stack_mem[sizeof(IOEvent)];
	Event *pevent;

	// 1 处理url
	QUrl URL( url );
	if( !URL.isValid() )//第一关都过不了
	{
		// 发送load failed事件
		pevent = new(stack_mem) IOEvent(
			Event::IOEVENT_LOAD_FAILED,
			url.toStdString().c_str(),
			1,
			"invalid url to download",
			NULL
		);
		eventHappen( *pevent );
		pevent->~Event();
		return NULL;
	}

	// 2 研究到底要打开的文件是什么(如果是http要打开临时文件，如果是本地资源要打开这个资源文件)
	string file_name = CACHE_DIR;
	if( URL.scheme().isEmpty() )//没有协议类型，那就当是本地文件处理了
	{
		URL = "file://"+url;
	}
	if( URL.scheme().toLower()=="file" )// 请求的是本地资源
	{
		int pos = URL.toString().indexOf("file://")+sizeof("file://")-1;
		file_name = URL.toString().mid(pos).toStdString();//要得到一个string好难

		return impl_->loadFile(file_name, buffer, buflen);
	}
	else if( URL.scheme().toLower().left(4) == "http" )//这个是正解，先检查url完整性
	{
		if( URL.host().isEmpty() )//没有host，我自认为你在本地架了个服务器
		{
			URL.setHost( "localhost" );
		}
		if( URL.path().isEmpty() || URL.path().right(1)=="/" )//没有文件名，按照http我给你加index
		{
			URL.setPath( URL.path()+"index.html" );
		}

		// 再检查是否已经有下载过这个文件了
		QByteArray hash_code = QCryptographicHash::hash( QUrl::toPercentEncoding( URL.toString() ), QCryptographicHash::Md5 );
		file_name += hash_code.toHex().constData();
		if( QFile::exists( file_name.c_str() ) )//已经有下载过了
		{
			return impl_->loadFile(file_name, buffer, buflen);
		}
		else
		{
			return impl_->doGetSave(URL.toString(), buffer, buflen, file_name);
		}
	}
	else // 这里是不支持的url
	{
		pevent = new(stack_mem) IOEvent(
			Event::IOEVENT_LOAD_FAILED,
			url.toStdString().c_str(),
			2,
			"url's scheme must be http(s)" " or pathname",
			NULL
		);
		eventHappen(*pevent);
		pevent->~Event();
		return NULL;
	}
}

}

#include "Spider.moc"
